/*
 * MIT License
 *
 * Copyright (c) 2019 Mr.css
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

package cn.seaboot.commons.file;

import cn.seaboot.commons.core.FastJsonUtils;
import cn.seaboot.commons.lang.P;
import org.jetbrains.annotations.NotNull;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;

/**
 * 相应内容写出工具
 * <p>
 * 添加了大量基础的函数
 * <p>
 * 文件下载流程：设置 Content-Type -> 设置  Content-Disposition -> 设置 Content-Length ->  IO 复制
 *
 * @author css 2018-07-10 11:56
 */
public class ResponseWriter {

    /**
     * UA
     */
    public static final String USER_AGENT = "User-Agent";

    /**
     * 内容长度
     */
    public static final String CONTENT_LENGTH = "Content-Length";

    /**
     * 内容配置
     */
    public static final String CONTENT_DISPOSITION = "Content-Disposition";

    /**
     * 将 Content-Type 设置为二进制流
     * <p>
     * 文件下载设置这个
     *
     * @param response -
     */
    public static void setMimeAsStream(@NotNull HttpServletResponse response) {
        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
    }

    /**
     * 设置 ContentType
     * <p>
     * 从文件名中提取扩展名，选择合适的 MIME 类型，作为 ContentType
     *
     * @param filename 文件名
     * @param response -
     */
    public static void setMimeFromName(@NotNull HttpServletResponse response, String filename) {
        String ext = FilenameUtils.getExtension(filename);
        String mime = MimeUtils.getMimeType(ext);
        if (mime == null) {
            mime = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        }
        response.setContentType(mime);
    }

    /**
     * 设置响应正文的长度
     * <p>
     * 设置响应内容长度，通常不设置也不会报错，部分浏览器可能无法识别，需要特别注意 chrome
     *
     * @param response -
     * @param length   内容长度
     */
    public static void setContentLength(@NotNull HttpServletResponse response, String length) {
        response.setHeader(CONTENT_LENGTH, length);
    }

    /**
     * 设置响应正文的长度
     * <p>
     * 设置响应内容长度，通常不设置也不会报错，部分浏览器可能无法识别，需要特别注意 chrome
     *
     * @param response -
     * @param length   内容长度
     */
    public static void setContentLength(@NotNull HttpServletResponse response, long length) {
        response.setHeader(CONTENT_LENGTH, Long.toString(length));
    }

    /**
     * 去除浏览器缓存
     * <p>
     * 对于相同请求、相同参数的请求，浏览器可能会自动优化，不再重复请求。
     * <p>
     * 想要避免产生这种情况，可以使用当前函数
     * <p>
     * Cache-Control 缓存控制
     * no-cache：指示请求或响应消息不能被缓存。
     * no-store：要求缓存机制在接收到此指令的情况下，不能存储请求或响应的任何内容。
     * must-revalidate：当与 max-age 一起使用时，指示一旦缓存过期，必须向服务器重新验证
     *
     * @param response -
     */
    public static void setNoCache(@NotNull HttpServletResponse response) {
        response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        // HTTP 1.0 兼容
        response.setHeader("Pragma", "no-cache");
        // 虽然现代浏览器推荐使用 Cache-Control，但 Expires 头部在一些老旧浏览器中仍然有效。
        response.setDateHeader("Expires", 0);
    }


    /**
     * 使用 URLEncoder 对文件名进行编码，避免产生中文乱码
     * <p>
     * 在做数据导出的时候，这个函数用得比较多，需要为输出的文件，提供一个方便阅读的文件名
     *
     * @param filename 文件名
     * @return 文件名
     */
    public static String encodeFileName(String filename, String encode) {
        try {
            // 这里直接指定编码方式，在 Content-Disposition 中，需要指定解码方式
            return URLEncoder.encode(filename, encode);
        } catch (UnsupportedEncodingException ignore) {
            // 文件名不是什么很重要的内容，这里忽视编码异常
            return filename;
        }
    }

    /**
     * 设置 Content-Disposition
     * <p>
     * Content-Disposition：内容配置
     * attachment：作为附件下载；
     * inline：在线预览，如果无法预览，则转为下载；
     * filename：只支持 ASCII 字符集，不支持中文；
     * filename*：支持非 ASCII 字符，遵循 RFC-5987 中规定的编码方式；
     *
     * @param name     文件下载用的文件名
     * @param response -
     */
    public static void setAttachName(@NotNull HttpServletResponse response, String name) {
        response.setHeader(CONTENT_DISPOSITION, "attachment;filename=" + name);
    }

    /**
     * 设置 Content-Disposition
     * <p>
     * Content-Disposition：内容配置
     * attachment：作为附件下载；
     * inline：在线预览，如果无法预览，则转为下载；
     * filename：只支持 ASCII 字符集，不支持中文；
     * filename*：支持非 ASCII 字符，遵循 RFC-5987 中规定的编码方式；
     *
     * @param name     文件下载用的文件名
     * @param encode   文件名编码
     * @param response -
     */
    public static void setAttachName(@NotNull HttpServletResponse response, String name, String encode) {
        String encoded = encodeFileName(name, encode);
        response.setHeader(CONTENT_DISPOSITION, "attachment;filename*=" + encode + "''" + encoded);
    }

    /**
     * 设置 Content-Disposition
     *
     * @param name     文件下载用的文件名
     * @param response -
     */
    public static void setInlineName(@NotNull HttpServletResponse response, String name) {
        response.setHeader(CONTENT_DISPOSITION, "inline;filename=" + name);
    }

    /**
     * 设置 Content-Disposition
     *
     * @param name     文件下载用的文件名
     * @param encode   文件名编码
     * @param response -
     */
    public static void setInlineName(@NotNull HttpServletResponse response, String name, String encode) {
        String encoded = encodeFileName(name, encode);
        response.setHeader(CONTENT_DISPOSITION, "inline;filename*=" + encode + "''" + encoded);
    }

    /**
     * 写出一段纯文本
     *
     * @param response -
     * @param content  内容
     * @throws IOException -
     */
    public static void writeText(@NotNull HttpServletResponse response, @NotNull String content) throws IOException {
        response.setContentType(MediaType.TEXT_HTML_VALUE);
        response.getWriter().print(content);
    }


    /**
     * 写出一段 html 源码
     *
     * @param response -
     * @param content  内容
     * @throws IOException -
     */
    public static void writeHtml(@NotNull HttpServletResponse response, @NotNull String content) throws IOException {
        response.setContentType(MediaType.TEXT_HTML_VALUE);
        response.getWriter().print(content);
    }


    /**
     * 写出一段 json
     *
     * @param response -
     * @param content  内容
     * @throws IOException -
     */
    public static void writeJson(@NotNull HttpServletResponse response, @NotNull Object content) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().print(content);
    }

    /**
     * 写出一段 json
     *
     * @param response -
     * @param content  内容
     * @throws IOException -
     */
    public static void writeJson(@NotNull HttpServletResponse response, @NotNull String content) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().print(FastJsonUtils.toJSONString(content));
    }

    /**
     * IO 流转接
     * <p>
     * 注意还需要设置 Content-Disposition 和 Content-Type
     *
     * @param response -
     * @param is       文件输入流
     * @throws IOException -
     */
    public static void copy(@NotNull HttpServletResponse response, InputStream is) throws IOException {
        IOUtils.copy(is, response.getOutputStream());
    }

    /**
     * 下载文件
     *
     * @param response -
     * @param is       文件输入流
     * @param name     文件名
     * @param encode   文件名编码
     * @throws IOException -
     */
    public static void download(@NotNull HttpServletResponse response, InputStream is, String name, String encode) throws IOException {
        setMimeAsStream(response);
        setAttachName(response, name, encode);
        IOUtils.copy(is, response.getOutputStream());
    }

    /**
     * 下载文件
     *
     * @param response -
     * @param is       文件输入流
     * @param name     文件名，中文会乱码
     * @throws IOException -
     */
    public static void download(@NotNull HttpServletResponse response, InputStream is, String name) throws IOException {
        setMimeAsStream(response);
        setAttachName(response, name);
        IOUtils.copy(is, response.getOutputStream());
    }

    /**
     * 下载文件
     *
     * @param response -
     * @param file     文件
     * @throws IOException -
     */
    public static void download(@NotNull HttpServletResponse response, File file) throws IOException {
        String fileName = file.getName();
        setMimeAsStream(response);
        setAttachName(response, fileName, P.filenameEncode());
        try (InputStream is = IOUtils.openFileInputStream(file)) {
            IOUtils.copy(is, response.getOutputStream());
        }
    }

    /**
     * 在线预览文件
     *
     * @param response -
     * @param is       文件输入流
     * @param name     文件名
     * @param encode   文件名编码
     * @throws IOException -
     */
    public static void preview(@NotNull HttpServletResponse response, InputStream is, String name, String encode) throws IOException {
        setMimeFromName(response, name);
        setInlineName(response, name, encode);
        IOUtils.copy(is, response.getOutputStream());
    }

    /**
     * 在线预览文件
     *
     * @param response -
     * @param is       文件输入流
     * @param name     文件名，中文会乱码
     * @throws IOException -
     */
    public static void preview(@NotNull HttpServletResponse response, InputStream is, String name) throws IOException {
        setMimeFromName(response, name);
        setInlineName(response, name);
        IOUtils.copy(is, response.getOutputStream());
    }

    /**
     * 在线预览文件
     *
     * @param response -
     * @param file     文件
     * @throws IOException -
     */
    public static void preview(@NotNull HttpServletResponse response, File file) throws IOException {
        String fileName = file.getName();
        setMimeFromName(response, fileName);
        setInlineName(response, fileName, P.filenameEncode());
        try (InputStream is = IOUtils.openFileInputStream(file)) {
            IOUtils.copy(is, response.getOutputStream());
        }
    }


    /**
     * 用户代理（User Agent，简称 UA）
     * <p>
     * 由浏览器上传的一段信息，包含操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。
     * <p>
     * 用于区分客户端，主要用于处理代码兼容相关的问题。
     * <p>
     * 不过并不十分可信，可以使用爬虫等工具伪造。
     *
     * @return 编码名称
     * @throws NullPointerException request is null
     */
    public static String getUserAgent(HttpServletRequest request) {
        return request.getHeader(USER_AGENT);
    }

    /**
     * 根据 user-agent 推测客户端可用的文件编码
     * <p>
     * 文件下载响应头，兼容IE的写法
     * Content-Disposition 解决中文名乱码
     * UTF-8 [firefox、chrome、safari、opera]
     * GBK [IE]
     * <p>
     * <p>android
     * <p>mozilla/5.0 (linux; android 10; oneplus a6000 build/qkq1.190716.003; wv) applewebkit/537.36 (khtml, like gecko) version/4.0 chrome/77.0.3865.120 mqqbrowser/6.2 tbs/045525 mobile safari/537.36 mmwebid/2340 micromessenger/8.0.1.1841(0x2800015d) process/tools wechat/arm64 weixin nettype/wifi language/zh_cn abi/arm64
     * <p>Chrome
     * <p>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36
     * <p>360
     * <p>Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
     * <p>IE
     * <p>Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko
     *
     * @return 编码名称
     * @throws NullPointerException request is null
     */
    public static String getFileNameCharset(HttpServletRequest request) {
        String userAgent = request.getHeader(USER_AGENT).toLowerCase();
        if (userAgent.contains("msie") || userAgent.contains("trident") || userAgent.contains("edge")) {
            // IE内核
            return "GBK";
        } else if (userAgent.contains("android") || userAgent.contains("adr")) {
            // android终端
            return "GBK";
        }
        return "UTF-8";
    }


    /**
     * 文件分段写出，一般用在视频、音频文件
     *
     * @param response -
     * @param rangeHeader 分段信息，E.G.: bytes=241532928-
     * @param file 需要下载的文件
     * @throws IOException -
     */
    public static void writeRange(HttpServletResponse response, @NotNull String rangeHeader, File file) throws IOException {
        // 视频分段写出？
        long fileSize = file.length();

        // 手动解析 Range 头（格式：bytes=0-1023）
        long start_idx = 0, end_idx = fileSize - 1;
        try {
            String[] ranges = rangeHeader.substring("bytes=".length()).split("-");
            start_idx = Long.parseLong(ranges[0]);
            if (ranges.length > 1) {
                end_idx = Long.parseLong(ranges[1]);
            }
        } catch (Exception e) {
            response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            return;
        }

        // 校验范围
        if (start_idx > end_idx || start_idx >= fileSize) {
            response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            return;
        }

        // 返回 206 Partial Content
        long contentLength = end_idx - start_idx + 1;
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        response.setContentLength((int) contentLength);
        response.setHeader("Content-Range", "bytes " + start_idx + "-" + end_idx + "/" + fileSize);
        response.setHeader("Accept-Ranges", "bytes");

        // 使用RandomAccessFile进行分块读取和写入
        try (RandomAccessFile raf = new RandomAccessFile(file, "r");
             OutputStream os = response.getOutputStream()) {

            // 定位到起始位置
            raf.seek(start_idx);

            // 缓冲区大小（建议使用8KB或更大）
            byte[] buffer = new byte[8192];
            long remaining = contentLength;
            int bytesRead;

            while (remaining > 0 &&
                    (bytesRead = raf.read(buffer, 0, (int) Math.min(buffer.length, remaining))) != -1) {
                try{
                    os.write(buffer, 0, bytesRead);
                } catch (IOException ignored){
                    // 视频分段读取的时候，客户端会主动中断，这时候这里会正常抛出异常
                    return;
                }
                remaining -= bytesRead;
            }
            os.flush();
        }
    }

}
